#* -coding: utf - 8 -* -
"""
 Copyright 2010 Kruglov M.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
"""

import baseplugins
import helpers
import os
import pwd
import select
import signal
import sys
import traceback
import xmpp

class BaseConfig(object):

    # UserInfo
    login = ""
    password = ""

    # System
    sysuser = "bot"
    sysgroup = "bot"
    umask = 022
    workdir = "/"
    logfile = sys.stderr
    pidfile = False
    logformat = "[%(date)s %(time)s] (%(level)s) %(logger)s: %(text)s"
    daemonize = 0
    xmppdebug = False
    loglevel = 0 # ni
    plugins = list()

    def check(self):
        if self.plugins.__class__.__name__ != 'list':
            print "config.plugins must be a list"
            return False
        else:
            return True

class LogMsg(object):

    loglevels = {
        'DEBUG': 0,
        'INFO': 1,
         'WARN': 2,
         'CRIT': 3,
         0: 'DEBUG',
         1: 'INFO',
         2: 'WARN',
         3: 'CRIT'}

    logger = "Kernel"

    def __init__(self,logformat,cfgloglevel,log):
        self.__cfgloglevel = cfgloglevel
        self.__logformat = logformat
        self.__setLogFile(log)

    def setTrace(self):
        if self.__cfgloglevel == 0:
            traceback.print_exc(file=self.__log)

    def changeLog(self,newlog):
        #~ if not self.__log.closed:
            #~ self.__log.close()
        self.__setLogFile(newlog)
        #~ self.__call__("logfile changed",0)

    def __setLogFile(self,x):
        try:
            x.flush()
            self.__log = x
        except AttributeError:
            self.__log = open(x,'w')

    def __call__(self,text,level=1):
        if self.__cfgloglevel <= level:
            caller = sys._getframe().f_back.f_locals['self']
            tmp = helpers.getTimeNow()
            time_now = "%(hour)s:%(min)s:%(sec)s" % tmp
            date_now = "%(year)s-%(mon)s-%(mday)s" % tmp
            fargs = {'date': date_now,'time': time_now,'text':text,'level': self.loglevels[level],'logger': caller.logger}
            self.__log.write(self.__logformat % fargs + "\n")
            self.__log.flush()

class Kernel(object):
    logger = "Kernel"

    # Plugins support
    def __registerPlugin(self,plugin):
        try:
            if plugin.name in self.loaded_plugins:
                self.logMessage("plugin '%s' already registered" % plugin.name,2)
                return False

            if plugin.plugin_type == "xmpp_handler":
                self.xmppHandlerRegister(plugin.handler_type,plugin.preOnCall,plugin.makefirst)
                if plugin.makefirst == 1:
                    self.logMessage("plugin '%s' registered as first xmpp handler '%s'" % (plugin.name,plugin.handler_type))
                else:
                    self.logMessage("plugin '%s' registered as xmpp handler '%s'" % (plugin.name,plugin.handler_type))
                self.__setattr__(plugin.name,plugin)
                return True
            elif plugin.plugin_type == "kernel":
                self.__setattr__(plugin.name,plugin)
                return True
            elif plugin.plugin_type == "periodic_callback":
                plugin.kern = self
                self.sheduler.setCallback(plugin)
                self.logMessage("plugin '%s' registered as periodic task, interval =  '%s'" % (plugin.name,plugin.timeout))
                if plugin.start_on_plug:
                    self.sheduler.start(plugin.name)
                return True

            else:
                self.logMessage("unknown plugin type '%s'" % plugin.plugin_type,2)
                return False
        except AttributeError,e:
            self.logMessage("can't register plugin '%s': %s" % (plugin.name,e),2)
            self.logMessage.setTrace()
            return False

    def __hotPlugObject(self,obj):
        if not obj.prePlug():
            raise RuntimeError("error importing plugin '%s', step prePlug" % obj.name,2)
            self.logMessage.setTrace()
        elif not self.__registerPlugin(obj):
            raise RuntimeError("error importing plugin '%s',step registerPlugin" % obj.name,2)
            self.logMessage.setTrace()
        elif not obj.afterPlug():
            raise RuntimeError("error importing plugin '%s', step afterPlug" % obj.name,2)
            self.logMessage.setTrace()
        else:
            self.__setattr__(obj.name,obj)

    def __loadPlugins(self,plugins_list):
        for plugin in plugins_list:
            obj_name = plugin.split('.')
            path = helpers.arrayToString(obj_name[0:-1],delimeter='.')
            cls = obj_name[-1]
            tmp = __import__(path,globals(),locals(),[cls,])

            try:
                obj = eval('tmp.' + cls + '(self)')
                if not obj.name:
                    obj.name = obj.__class__.__name__
                self.__hotPlugObject(obj)
                self.logMessage("plugin '%s' successfully loaded" % obj.name)
                self.loaded_plugins.append(obj.name)
            except RuntimeError,e:
                self.logMessage("can't load plugin %s: %s" % (obj.name,e),2)
                self.logMessage.setTrace()

        return True

    def loadPlugin(self,obj):
        if not obj.name:
            obj.name = obj.__class__.__name__
        try:
            self.__hotPlugObject(obj)
            self.logMessage("plugin '%s' successfully loaded" % obj.name)
            self.loaded_plugins.append(obj.name)
        except RuntimeError,e:
            self.logMessage("can't load plugin %s: %s" % (obj.name,e),2)
            self.logMessage.setTrace()

    def daemonize(self):

        """
        Detach a process from the controlling terminal and run it in the
        background as a daemon.
        """

        UMASK = self.config.umask
        WORKDIR = self.config.workdir
        MAXFD = 1024

        if (hasattr(os,"devnull")):
            redirect_to = os.devnull
        else:
            redirect_to = "/dev/null"

        try:
            pid = os.fork()
        except OSError,e:
            raise Exception,"%s [%d]" % (e.strerror,e.errno)
        if (pid == 0):   # The first child.
            os.setsid()
            signal.signal(signal.SIGHUP,signal.SIG_IGN)
            try:
                pid = os.fork()    # Fork a second child.
            except OSError,e:
                raise Exception,"%s [%d]" % (e.strerror,e.errno)
            if (pid == 0):    # The second child.
                os.chdir(WORKDIR)
                os.umask(UMASK)
            else:
                os._exit(0)
        else:
            os._exit(0)
        import resource
        maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
        if (maxfd == resource.RLIM_INFINITY):
            maxfd = MAXFD
        for fd in range(0,maxfd):
            try:
                os.close(fd)
            except OSError:
                pass
        os.open(redirect_to,os.O_RDWR)
        os.dup2(0,1)
        os.dup2(0,2)
        pid = open(self.config.pidfile,'w')
        pid.write(str(os.getpid()))
        pid.close()
        self.logMessage.changeLog(self.config.logfile)
        pid = open(self.config.pidfile,'w')
        pid.write(str(os.getpid()))
        pid.close()

    def sigStopHandler(self,signum,frame):
        self.logMessage("shutdown request")
        if self.config.pidfile and os.path.isfile(self.config.pidfile):
            os.unlink(self.config.pidfile)
        cbs = self.sheduler.callbacks.copy()
        for cb in cbs:
            self.sheduler.delCallback(cb)
        self.online = 0
        self.logMessage("stopping threads")


    # XMPP support
    def xmppHandlerRegister(self,type,func,insert_first):
        self.bot.RegisterHandler(type,func,makefirst=insert_first)

    def sendMessage(self,to,text):
        if isinstance(to,str):
            message = xmpp.Message(xmpp.JID(to),text)
        else:
            message = xmpp.Message(to,text)
        self.bot.send(message)

    # XMPP connect
    def connect(self):
        try:
            jid = xmpp.JID(self.config.login)
        except ValueError:
            self.logMessage("login not valid",3)
            sys.exit(1)

        if self.config.xmppdebug:
            self.bot = xmpp.Client(jid.getDomain())
        else:
            self.bot = xmpp.Client(jid.getDomain(),debug=[])

        if self.bot.connect():
            self.logMessage("connect ok")
        else:
            self.logMessage("connect error!")
            self.bot.disconnect()
            sys.exit(1)

        if self.bot.auth(jid.getNode(),self.config.password,jid.getResource()):
            self.logMessage("auth ok")
        else:
            self.logMessage("auth error!")
            self.bot.disconnect()
            sys.exit(1)
        self.__loadPlugins(self.config.plugins)

        self.bot.sendInitPresence(requestRoster=0)
        self.online = 1
        self.logMessage("bot started")

    # init
    def __init__(self,config):
        self.online = 0
        self.sandbox = helpers.Sandbox()
        if not config.check():
            print >> sys.stderr,"config error, exiting"
            sys.exit()
        else:
            self.config = config

        self.logMessage = LogMsg(self.config.logformat,self.config.loglevel,log=self.config.logfile)
        os.umask(self.config.umask)

        self.sheduler = baseplugins.Sheduler(self)
        self.sheduler.kern = self
        self.loaded_plugins = list()

        if self.config.daemonize:
            self.daemonize()
            if self.config.pidfile and os.path.isfile(self.config.pidfile):
                self.logMessage("PID file %s exists, is bot running?" % self.config.pidfile,3)
                sys.exit(1)
            if os.geteuid() == 0 and self.config.sysgroup:
                self.logMessage("changing EGID to %s" % self.config.sysgroup,0)
                os.setegid(pwd.getpwnam(self.config.sysgroup)[2])
            if os.geteuid() == 0 and self.config.sysuser:
                self.logMessage("changing EUID to %s" % self.config.sysgroup,0)
                os.seteuid(pwd.getpwnam(self.config.sysuser)[2])

        signal.signal(signal.SIGINT,self.sigStopHandler)
        signal.signal(signal.SIGTERM,self.sigStopHandler)
        #~ signal.signal(signal.SIGQUIT,self.sigStopHandler)

    # states
    def run(self):
        while self.online:
            try:
                self.bot.Process(1)
            except select.error:
                pass
        self.logMessage("main loop stopped")
        self.bot.disconnect()
        self.logMessage("bot disconnected")

    def serve(self):
        if not self.online:
            self.connect()
        self.run()

    def stop(self):
        os.kill(os.getpid(),signal.SIGTERM)

    #In progress ...
    def botBeforeStartHandlerRegister(self,fn):
        self.start_handlers.append(fn)
    def sigReloadHandler(self,signum,frame):
        self.logMessage("reload request")
        self.logMessage("reload not yet implemented")

